import json
import logging
from collections import deque

import time
from typing import Text, Optional, Union, Deque, Dict, Any

logger = logging.getLogger(__name__)

NO_TICKET_ISSUED = -1  # 最新票证的索引（如果没有票证）


class Ticket:
    def __init__(self, number: int, expires: float) -> None:
        self.number = number
        self.expires = expires

    def has_expired(self) -> bool:
        return time.time() > self.expires

    def as_dict(self) -> Dict[Text, Any]:
        return dict(number=self.number, expires=self.expires)

    def dumps(self) -> Text:
        """返回“ Ticket”的json转储为字典。"""

        return json.dumps(self.as_dict())

    @classmethod
    def from_dict(cls, data: Dict[Text, Union[int, float]]) -> "Ticket":
        """从字典创建`Ticket`。"""

        return cls(number=data["number"], expires=data["expires"])

    def __repr__(self) -> Text:
        return f"Ticket(number: {self.number}, expires: {self.expires})"


class TicketLock:
    """发出票证以管理对会话ID的访问的锁定机制。

    票证按照请求的顺序发行。 有关票证锁定算法的详细说明，
    请参见http://pages.cs.wisc.edu/~remzi/OSTEP/threads-locks.pdf#page=13
    """

    def __init__(
        self, conversation_id: Text, tickets: Optional[Deque[Ticket]] = None
    ) -> None:
        self.conversation_id = conversation_id
        self.tickets = tickets or deque()

    @classmethod
    def from_dict(cls, data: Dict[Text, Any]) -> "TicketLock":
        """从字典中创建 “TicketLock”。"""

        tickets = [Ticket.from_dict(json.loads(d)) for d in data.get("tickets")]
        return cls(data.get("conversation_id"), deque(tickets))

    def dumps(self) -> Text:
        """返回`TicketLock`的json转储。"""

        tickets = [ticket.dumps() for ticket in self.tickets]
        return json.dumps(dict(conversation_id=self.conversation_id, tickets=tickets))

    def is_locked(self, ticket_number: int) -> bool:
        """返回“ ticket_number”是否被锁定。

        Returns:
               如果“ now_serving”不等于“ ticket”，则为true。
        """

        return self.now_serving != ticket_number

    def issue_ticket(self, lifetime: float) -> int:
        """发行新票并返回其编号。"""

        self.remove_expired_tickets()
        number = self.last_issued + 1
        ticket = Ticket(number, time.time() + lifetime)
        self.tickets.append(ticket)

        return number

    def remove_expired_tickets(self) -> None:
        """删除过期的票证。"""

        # 遍历self.tickets的副本，以便我们可以删除项目
        for ticket in list(self.tickets):
            if ticket.has_expired():
                self.tickets.remove(ticket)

    @property
    def last_issued(self) -> int:
        """返回上次添加的故障单的编号。

        Returns:
             上次添加的“门票”的数量。 如果没有票证，则为“ NO_TICKET_ISSUED”。
        """

        ticket_number = self._ticket_number_for(-1)

        return ticket_number if ticket_number is not None else NO_TICKET_ISSUED

    @property
    def now_serving(self) -> Optional[int]:
        """获取接下来要送达的机票编号。

        Returns:
             接下来投放的“票务”编号。 如果不存在“票证”，则为0。
        """

        return self._ticket_number_for(0) or 0

    def _ticket_number_for(self, ticket_index: int) -> Optional[int]:
        """获取`ticket_index`的票证编号。

        Returns:
             带有“ ticket_index”索引的“ Ticket”的票号。
             如果没有票证，或者“ ticket_index”超出了“ self.tickets”的范围，则为“无”。
        """

        self.remove_expired_tickets()

        try:
            return self.tickets[ticket_index].number
        except IndexError:
            return None

    def _ticket_for_ticket_number(self, ticket_number: int) -> Optional[Ticket]:
        """Return ticket for `ticket_number`."""

        self.remove_expired_tickets()

        return next((t for t in self.tickets if t.number == ticket_number), None)

    def is_someone_waiting(self) -> bool:
        """Return whether someone is waiting for the lock to become available.

        Returns:
             True if the `self.tickets` queue has length greater than 0.
        """

        return len(self.tickets) > 0

    def remove_ticket_for(self, ticket_number: int) -> None:
        """Remove `Ticket` for `ticket_number."""

        ticket = self._ticket_for_ticket_number(ticket_number)
        if ticket:
            self.tickets.remove(ticket)
